using System;
using System.Reflection;
using System.Reflection.Emit;

namespace PropertyExpression.ControllableQuery
{
    public static class Qwe
    {
        private static readonly ModuleBuilder moduleBuilder;

        static Qwe()
        {
            var assemblyName = new AssemblyName { Name = Guid.NewGuid().ToString() };
            moduleBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run).
                DefineDynamicModule(assemblyName.Name);
        }

        public static Func<T, TResult> BuildDynamicTypeWithProperties<T, TResult>(Func<Type, MethodInfo> func)
        {
            var typeBuilder = moduleBuilder.DefineType(
                typeof(TResult).Name + "_" + Guid.NewGuid(), TypeAttributes.NotPublic, null, new[] { typeof(TResult) });

            foreach (var property in typeof(TResult).GetProperties())
            {
                CreateProperty(typeBuilder, property);
            }

            var type = typeBuilder.CreateType();

            var dynamicMethod = new DynamicMethod(Guid.NewGuid().ToString(),
                                                  type,
                                                  new[] { typeof(T) },
                                                  true);

            var ilGenerator = dynamicMethod.GetILGenerator();
            ilGenerator.DeclareLocal(type);
            ilGenerator.Emit(OpCodes.Newobj, type.GetConstructor(new Type[] { }));
            ilGenerator.Emit(OpCodes.Stloc_0);
            foreach (var property in type.GetProperties())
            {
                ilGenerator.Emit(OpCodes.Ldloc_0);
                ilGenerator.Emit(OpCodes.Ldarg_0);
                ilGenerator.Emit(OpCodes.Ldstr, property.Name);
                ilGenerator.EmitCall(OpCodes.Call, func(property.PropertyType), null);
                ilGenerator.EmitCall(OpCodes.Callvirt, property.GetSetMethod(), null);
            }
            ilGenerator.Emit(OpCodes.Ldloc_0);
            ilGenerator.Emit(OpCodes.Ret);

            dynamicMethod.DefineParameter(1, ParameterAttributes.In, "arg1");

            return (Func<T, TResult>)dynamicMethod.CreateDelegate(typeof(Func<T, TResult>));
        }

        private static void CreateProperty(TypeBuilder typeBuilder, PropertyInfo propertyInfo)
        {
            var fieldBuilder = typeBuilder.DefineField(
                GetPascalCase(propertyInfo.Name), propertyInfo.PropertyType, FieldAttributes.Private);

            var propertyBuilder = typeBuilder.DefineProperty(
                propertyInfo.Name, PropertyAttributes.None, propertyInfo.PropertyType, null);

            const MethodAttributes getSetMethodAttributes =
                MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig
                | MethodAttributes.SpecialName;

            var getterMethodBuilder = typeBuilder.DefineMethod("get_" + propertyInfo.Name,
                                                               getSetMethodAttributes,
                                                               propertyInfo.PropertyType,
                                                               Type.EmptyTypes);

            var getterIlGenerator = getterMethodBuilder.GetILGenerator();
            getterIlGenerator.Emit(OpCodes.Ldarg_0);
            getterIlGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
            getterIlGenerator.Emit(OpCodes.Ret);

            var setterMethodBuilder = typeBuilder.DefineMethod("set_" + propertyInfo.Name,
                                                               getSetMethodAttributes,
                                                               null,
                                                               new[] { propertyInfo.PropertyType });

            var setterIlGenerator = setterMethodBuilder.GetILGenerator();
            setterIlGenerator.Emit(OpCodes.Ldarg_0);
            setterIlGenerator.Emit(OpCodes.Ldarg_1);
            setterIlGenerator.Emit(OpCodes.Stfld, fieldBuilder);
            setterIlGenerator.Emit(OpCodes.Ret);

            propertyBuilder.SetGetMethod(getterMethodBuilder);
            propertyBuilder.SetSetMethod(setterMethodBuilder);
        }

        private static string GetPascalCase(string name)
        {
            return name.Substring(0, 1).ToLower() + name.Substring(1);
        }
    }
}